/*
 * 风扇控制器 V1开源版
 * Designed by ZhangHao
 * Create：2020.11.1
 * 
 * Note:
 * 1 自动配网：Smart Config；
 * 2 OTA升级；
 * 3 风扇控制器驱动函数
 * 4 DS18B20温度读取函数
 * 5 响应命令和事件  温度闭环迟滞控制
 *
 * 详见ReadMe.md文件
 */

/*
include files
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <Ticker.h>
#include <ArduinoJson.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <OneWire.h>
#include <DallasTemperature.h>

/*
* 引脚定义  2020.11.1 嘉立创第一版硬件
* 工程硬件设计文件地址：
* Author：ZhangHao
*/
#define DIN D0          //DS18B20数据引脚   外部通过5.1K电阻上拉至3.3V
#define FUN_1_IN1 D1    //第一路风扇IN1脚
#define FUN_1_IN2 D2    //第一路风扇IN2脚
#define FUN_2_IN2 D5    //第二路风扇IN2脚
#define FUN_2_IN1 D6    //第二路风扇IN1脚
#define KEY1 D7         //网络配置按钮/切换按钮

/**
 *  用户结构体 
 **/
typedef struct funControal_MCB_t
{
  /*  属性  */
  uint8_t MAC_adress[6];        // 在SetUp函数中初始化该数组，查询时返回该数组
  char Version[9];              // 设备固件版本号
  int fun1Speed;                // 风扇1转速
  int fun2Speed;                // 风扇2转速
  float temperature;            // 温度传感器读数
  bool autoControl;             // 自动控制选项  
  float autoControlTemp;        // 自动控制起始温度  超过这一温度才会启动风扇
  bool autoReport;              // 自动上报选项
  bool tempAlarm;               // 是否高温报警
  float tempLimit;              // 高温报警阈值

  /*  Flags  */
  bool firstReport;             // 是否完成首次属性上报

};

/**
 * MQTT Topic List
 **/
//下行Topic
const char *readDevPropertiesTopic = "/funCon/funCon-1/properties/read";   //读取设备属性
const char *writeDevPropertiesTopic = "/funCon/funCon-1/properties/write"; //修改设备属性
const char *invokeDevFunctionTopic = "/funCon/funCon-1/function/invoke";   //调用设备功能

//上行Topic
const char *replyReadDevPropertiesTopic = "/funCon/funCon-1/properties/read/reply";   //读取属性回复
const char *replyWriteDevPropertiesTopic = "/funCon/funCon-1/properties/write/reply"; //修改属性回复
const char *replyInvokeDevFunctionTopic = "/funCon/funCon-1/function/invoke/reply";   //调用设备功能回复

//上报设备事件：电量报警
const char *reportTmpAlarmEventTopic = "/funCon/funCon-1/event/tmpAlarm";

//上报设备属性
const char *reportDevPropertiesTopic = "/funCon/funCon-1/properties/report";

//Function List
void report_data();
uint64_t get_time_stamp_ms();
float get_temperature();
void update_time();
void set_fun1_speed(int speed);
void set_fun2_speed(int speed);
void tempC_adj();

//Global variables 全局变量
const char *mqtt_server = "10.0.0.26"; //本地JetLinks MQTT网络组件
WiFiClient espClient;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com", 8*3600, 60000);
PubSubClient client(espClient);
Ticker reportDataTimer; // 上报属性定时器
Ticker temperatureMonitor; // 温度定期查询、控制风扇定时器
Ticker reflashNTP;
OneWire oneWire(DIN);
DallasTemperature sensors(&oneWire);
funControal_MCB_t gFunControlMCB;

struct mytime
{
  unsigned long rtcSecond;
  unsigned long ms;
};
struct mytime timems;

/**
 * 获取DS18B20温度函数
 * 返回float型温度  摄氏度
 **/
float get_temperature()
{
  sensors.requestTemperatures(); // Send the command to get temperatures
  float tempC = sensors.getTempCByIndex(0);
  if(tempC != DEVICE_DISCONNECTED_C) 
  {
    return tempC;
  } 
  else
  {
    Serial.println("Error: Could not read temperature data");
    return 0.0;
  }
}

/**
 * 设置风扇1的速度，输入范围-100~100
 * 输入负数风扇反转  正数正转  0不转
 **/
void set_fun1_speed(int speed)
{
  if (speed > 100 || speed < -100) 
  {
    Serial.print("[Set_Fun1_Speed] Illegal parameter:");
    Serial.println(speed);
    return;
  }
  if (speed == 0)
  {
    /* 停止风扇 */
    digitalWrite(FUN_1_IN1, LOW);
    digitalWrite(FUN_1_IN2, LOW);
    Serial.print("[Set_Fun1_Speed] STOP!");
  }
  if (speed > 0)
  {
    digitalWrite(FUN_1_IN2, LOW);
    analogWrite(FUN_1_IN1, speed*10);
    Serial.print("[Set_Fun1_Speed] GO!");
  }
  if (speed < 0)
  {
    speed = abs(speed);
    digitalWrite(FUN_1_IN1, LOW);
    analogWrite(FUN_1_IN2, speed*10);
    Serial.print("[Set_Fun1_Speed] BACK!");
  }
  return;
}
/**
 * 设置风扇2的速度，输入范围-100~100
 * 输入负数风扇反转  正数正转  0不转
 **/
void set_fun2_speed(int speed)
{
  if (speed > 100 || speed < -100) 
  {
    Serial.print("[Set_Fun2_Speed] Illegal parameter:");
    Serial.println(speed);
    return;
  }
  if (speed == 0)
  {
    /* 停止风扇 */
    digitalWrite(FUN_2_IN1, LOW);
    digitalWrite(FUN_2_IN2, LOW);
  }
  if (speed > 0)
  {
    digitalWrite(FUN_2_IN2, LOW);
    analogWrite(FUN_2_IN1, speed*10);
  }
  if (speed < 0)
  {
    speed = abs(speed);
    digitalWrite(FUN_2_IN1, LOW);
    analogWrite(FUN_2_IN2, speed*10);
  }
  return;
}



/**
 * 每10S调用一次  监测温度是否超过报警值
 * 风扇自动启停和转速控制
 **/
void tempC_adj()
{
  if(gFunControlMCB.autoControl == false)
  {
    return;
  }
  float tempC = get_temperature();
  if (tempC <  gFunControlMCB.autoControlTemp)
  {
    /* 小于风扇启动温度  关闭风扇后返回 */
    set_fun1_speed(0);
    set_fun2_speed(0);
    return;
  }
  if (tempC >  gFunControlMCB.autoControlTemp && tempC < gFunControlMCB.tempLimit)
  {
    float k = float(100/(gFunControlMCB.tempLimit - gFunControlMCB.autoControlTemp));
    float b = -k*gFunControlMCB.autoControlTemp;
    int speed = int(k*tempC + b);
    set_fun1_speed(speed);
    set_fun2_speed(speed);
  }
  
  if (tempC > gFunControlMCB.tempLimit)
  {
    /* 超过了最大温度限制  报警  风扇全速*/
    DynamicJsonDocument doc(200);
    char payload[200] = {'\0'};
  
    doc["messageId"] = "123456";
    doc["timestamp"] = get_time_stamp_ms();
    doc["data"] = tempC;
    serializeJson(doc, payload);
    Serial.print("JsonLen:");
    Serial.println(measureJson(doc));
    client.publish(reportTmpAlarmEventTopic, payload);

    set_fun1_speed(1000);
    set_fun2_speed(1000);
  }
}

/**
 * 更新时间函数
 * 每100秒调用一次
 **/
void update_time()
{
  Serial.print("[NTP] Upadte time, now is ");
  timeClient.update();
  Serial.println(timeClient.getFormattedTime());
  timems.rtcSecond = timeClient.getEpochTime() - 8*3600;//矫正时区
  timems.ms = millis();
  Serial.print("rtcsecond & ms:");
  Serial.println(timems.rtcSecond);
  Serial.println(timems.ms);
}

/**
 * 配网函数
 * 使用WiFi库+微信小程序获取ssid和password
**/
void smartConfig()
{
  WiFi.mode(WIFI_STA);
  Serial.println("\r\n[SmartConfig] Wait for Smartconfig...");
  WiFi.beginSmartConfig();
  while (1)
  {
    Serial.print(".");
    delay(1000); // wait for a second
    if (WiFi.smartConfigDone())
    {
      Serial.println("[SmartConfig] SmartConfig Success");
      Serial.printf("[SmartConfig] SSID:%s\r\n", WiFi.SSID().c_str());
      Serial.printf("PSW:%s\r\n", WiFi.psk().c_str());
      WiFi.setAutoConnect(true); // 设置自动连接，这个语句是最重要的语句。
      break;
    }
  }
}

/**
 * 自动配置网络
 * 当flash中有ssid和password时  可以自动上网
 * 否则调用smartConfig获取ssid和password
 **/
bool AutoConfig()
{
  WiFi.begin();
  for (int i = 0; i < 20; i++)
  {
    int wstatus = WiFi.status();
    if (wstatus == WL_CONNECTED)
    {
      Serial.println("[AutoConfig] WIFI AutoConfig Success");
      Serial.printf("[AutoConfig] SSID:%s", WiFi.SSID().c_str());
      Serial.printf(", PSW:%s\r\n", WiFi.psk().c_str());
      Serial.print("LocalIP:");
      Serial.print(WiFi.localIP());
      Serial.print(" ,GateIP:");
      Serial.println(WiFi.gatewayIP());
      return true;
    }
    else
    {
      Serial.print("[AutoConfig] WIFI AutoConfig Waiting......");
      Serial.println(wstatus);
      delay(1000);
    }
  }
  Serial.println("[AutoConfig] WIFI AutoConfig Faild!");
  return false;
}

//OTA升级  参数为要升级的文件路径
void OTA_Update(char *url)
{
  Serial.print("[OTA] OTA_URL:");
  Serial.println(url);

  //如果是0   恢复成默认固件  默认固件只支持OTA功能
  //固件存储在阿里云OSS和自建服务器
  if (url[0] == 0)
  {
    sprintf(url, "http://xx.xx.xx/xx.bin");
  }
  t_httpUpdate_return ret = ESPhttpUpdate.update(url);
  switch (ret)
  {
  case HTTP_UPDATE_FAILED:
    Serial.println("[OTA] Update failed.");
    break;
  case HTTP_UPDATE_NO_UPDATES:
    Serial.println("[OTA] Update no Update.");
    break;
  case HTTP_UPDATE_OK:
    Serial.println("[OTA] Update ok."); // may not called we reboot the ESP
    break;
  }
}

uint64_t get_time_stamp_ms()
{
  uint64_t timestamp = uint64_t(timems.rtcSecond) * 1000 + uint64_t(millis() - timems.ms);
  char tempstr[100] = {'\0'};
  sprintf(tempstr,"timpstamp:%lld",timestamp);
  Serial.println(tempstr);
  return timestamp;
}


//主动上报属性函数
//如果autoReport的值为TRUE，则每隔30秒上报一次属性
void report_data()
{
  if (gFunControlMCB.autoReport == false)
  {
    /* 如果不需要主动上报  直接退出 */
    return;
  }
  
  DynamicJsonDocument doc(200);
  char payload[200] = {'\0'};
  char mac[18] = {'\0'};
  sprintf(mac, "%X:%X:%X:%X:%X:%X", gFunControlMCB.MAC_adress[0], gFunControlMCB.MAC_adress[1], gFunControlMCB.MAC_adress[2], gFunControlMCB.MAC_adress[3], gFunControlMCB.MAC_adress[4], gFunControlMCB.MAC_adress[5]);
  //如果还未完成开机首次属性上报
  if (gFunControlMCB.firstReport == false)
  {
    doc["properties"]["devVersion"] = gFunControlMCB.Version;
    doc["properties"]["devMAC"] = mac;
    gFunControlMCB.firstReport = true;
    doc["properties"]["autoControl"] = gFunControlMCB.autoControl;
    doc["properties"]["autoControlTemp"] = gFunControlMCB.autoControlTemp;
    doc["properties"]["autoReport"] = gFunControlMCB.autoReport;
    doc["properties"]["tempAlarm"] = gFunControlMCB.tempAlarm;
    doc["properties"]["tempLimit"] = gFunControlMCB.tempLimit;
  }
  //每次都上报的数据
  doc["properties"]["fun1Speed"] = gFunControlMCB.fun1Speed;
  doc["properties"]["fun2Speed"] = gFunControlMCB.fun2Speed;
  gFunControlMCB.temperature = get_temperature();
  doc["properties"]["temperature"] = gFunControlMCB.temperature;

  //构建数据包上传
  doc["messageId"] = "123456";
  doc["timestamp"] = get_time_stamp_ms();
  serializeJson(doc, payload);
  Serial.print("JsonLen:");
  Serial.println(measureJson(doc));
  client.publish(reportDevPropertiesTopic, payload);
}


//MQTT消息的回调函数，用来处理收到的消息
void callback(char *topic, byte *payloadbyte, unsigned int length)
{
  DynamicJsonDocument doc(512);
  DynamicJsonDocument rpdoc(512);
  char payload[256] = {'\0'};
  unsigned int i;
  //向串口打印收到的消息
  Serial.print("[MQTT Massage] Topic->");
  Serial.print(topic);
  Serial.print(";Len->");
  Serial.print(length);
  Serial.println(".");
  Serial.print("Payload->");
  for (i = 0; i < length; i++)
  {
    payload[i] = (char)payloadbyte[i];
    Serial.print((char)payload[i]);
  }
  Serial.println();
  DeserializationError err = deserializeJson(doc, payload);
  if (err)
  {
    /* 解析JSON出错 */

    return;
  }
  //转换为topicStr
  String topicStr(topic);

  if (topicStr.equals(readDevPropertiesTopic))
  {
    /* 接收到读取设备属性的消息 */
    JsonArray arr = doc.getMember("properties");
    JsonObject properties = rpdoc.createNestedObject("properties");
    String tempStr;
    for (JsonVariant value : arr)
    {
      tempStr = String(value.as<char *>());
      //查询设备版本号
      if (tempStr.equals("devVersion"))
      {
        properties["devVersion"] = gFunControlMCB.Version;
      }
      //查询设备MAC地址
      if (tempStr.equals("devMAC"))
      {
        char mac[18] = {'\0'};
        sprintf(mac, "%X:%X:%X:%X:%X:%X", gFunControlMCB.MAC_adress[0], gFunControlMCB.MAC_adress[1], gFunControlMCB.MAC_adress[2], gFunControlMCB.MAC_adress[3], gFunControlMCB.MAC_adress[4], gFunControlMCB.MAC_adress[5]);
        properties["devMAC"] = mac;
      }
      if (tempStr.equals("fun1Speed"))
      {
        properties["fun1Speed"] = gFunControlMCB.fun1Speed;
      }
      if (tempStr.equals("fun2Speed"))
      {
        properties["fun2Speed"] = gFunControlMCB.fun2Speed;
      }
      if (tempStr.equals("temperature"))
      {
        gFunControlMCB.temperature = get_temperature();
        properties["temperature"] = gFunControlMCB.temperature;
      }
      if (tempStr.equals("autoReport"))
      {
        properties["autoReport"] = gFunControlMCB.autoReport;
      }
      if (tempStr.equals("autoControl"))
      {
        properties["autoControl"] = gFunControlMCB.autoControl;
      }
      if (tempStr.equals("autoControlTemp"))
      {
        properties["autoControlTemp"] = gFunControlMCB.autoControlTemp;
      }
      if (tempStr.equals("tempAlarm"))
      {
        properties["tempAlarm"] = gFunControlMCB.tempAlarm;
      }
      if (tempStr.equals("tempLimit"))
      {
        properties["tempLimit"] = gFunControlMCB.tempLimit;
      }
    }

    //增加必要的信息
    rpdoc["messageId"] = doc["messageId"];
    rpdoc["deviceId"] = doc["deviceId"];
    rpdoc["success"] = true;
    serializeJson(rpdoc, payload);
    Serial.println(payload);
    //推送到服务器
    client.publish(replyReadDevPropertiesTopic, payload);
    return;
  }

  if (topicStr.equals(writeDevPropertiesTopic))
  {
    /* 接收到修改设备属性的消息 */
    JsonObject properties = doc["properties"].as<JsonObject>();
    JsonObject rpproperties = rpdoc.createNestedObject("properties");
    String tempKey;
    for (JsonPair kv : properties)
    {
      tempKey = String(kv.key().c_str());
      //修改风扇1转速
      if (tempKey.equals("fun1Speed"))
      {
        gFunControlMCB.fun1Speed = kv.value().as<int>();
        //修改转速
        set_fun1_speed(gFunControlMCB.fun1Speed);
        rpproperties["fun1Speed"] = gFunControlMCB.fun1Speed;
      }
      //修改风扇2转速
      if (tempKey.equals("fun2Speed"))
      {
        gFunControlMCB.fun2Speed = kv.value().as<int>();
        set_fun2_speed(gFunControlMCB.fun2Speed);
        rpproperties["fun2Speed"] = gFunControlMCB.fun2Speed;
      }
      if (tempKey.equals("autoControlTemp"))
      {
        gFunControlMCB.autoControlTemp = kv.value().as<int>();
        rpproperties["autoControlTemp"] = gFunControlMCB.autoControlTemp;
      }
      if (tempKey.equals("tempLimit"))
      {
        gFunControlMCB.tempLimit = kv.value().as<int>();
        rpproperties["tempLimit"] = gFunControlMCB.tempLimit;
      }
      if (tempKey.equals("autoControl"))
      {
        gFunControlMCB.autoControl = kv.value().as<bool>();
        rpproperties["autoControl"] = gFunControlMCB.autoControl;
      }
      if (tempKey.equals("autoReport"))
      {
        gFunControlMCB.autoReport = kv.value().as<bool>();
        rpproperties["autoReport"] = gFunControlMCB.autoReport;
      }
      if (tempKey.equals("tempAlarm"))
      {
        gFunControlMCB.tempAlarm = kv.value().as<bool>();
        rpproperties["tempAlarm"] = gFunControlMCB.tempAlarm;
      }
    }
    //增加必要的信息
    rpdoc["messageId"] = doc["messageId"];
    rpdoc["success"] = true;
    serializeJson(rpdoc, payload);
    Serial.println(payload);
    //推送到服务器
    client.publish(replyWriteDevPropertiesTopic, payload);
    return;
  }

  if (topicStr.equals(invokeDevFunctionTopic))
  {
    /* 接收到调用设备功能消息 */
    String devFunction = doc["function"].as<String>();
    Serial.print("[Function]");
    Serial.println(devFunction);
    //OTA Upadte
    if (devFunction.equals("OTA"))
    {
      Serial.println("[Start OTA]");
      //OTA
      serializeJson(doc,Serial);
      String url = doc["inputs"][0]["value"].as<String>();
      Serial.print("[OTA] URL:");
      Serial.println(url);
      //回复云端
      rpdoc["messageId"] = doc["messageId"];
      rpdoc["success"] = true;
      rpdoc["output"] = 1;
      serializeJson(rpdoc, payload);
      Serial.println(payload);
      //推送到服务器
      client.publish(replyInvokeDevFunctionTopic, payload);
      t_httpUpdate_return ret = ESPhttpUpdate.update(url);
      switch (ret)
      {
      case HTTP_UPDATE_FAILED:
        Serial.println("[OTA] Update failed.");
        break;
      case HTTP_UPDATE_NO_UPDATES:
        Serial.println("[OTA] Update no Update.");
        break;
      case HTTP_UPDATE_OK:
        Serial.println("[OTA] Update ok."); // may not called we reboot the ESP
        break;
      }
      return;
    }
  }
}

//MQTT自动重连函数
void reconnect()
{
  // Loop until we're reconnected
  while (!client.connected())
  {
    Serial.print("Attempting MQTT connection...");
    // ClientId : 设备实例ID
    String clientId = "funCon-1";
    // UserName : secureId+"|"+timestamp
    String MQTTusername = "funConID|1598344006000";
    // PassWord : secureId+"|"+timestamp+"|"+secureKey
    // ToDo:
    String MQTTpassword = "e10c70d5350c33aa3962957a25958ae3";
    // Attempt to connect
    if (client.connect(clientId.c_str(), MQTTusername.c_str(), MQTTpassword.c_str()))
    {
      Serial.println("connected");
      //subscribe Topics
      client.subscribe(readDevPropertiesTopic);
      client.subscribe(writeDevPropertiesTopic);
      client.subscribe(invokeDevFunctionTopic);
    }
    else
    {
      Serial.print("MQTT Connect failed, rc=");
      Serial.print(client.state());
      Serial.println("Try again in 5 seconds...");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup()
{
  //初始化串口  输出日志
  Serial.begin(115200);

  //初始化IO口
  pinMode(KEY1, INPUT_PULLUP);
  pinMode(FUN_1_IN1, OUTPUT);
  pinMode(FUN_1_IN2, OUTPUT);
  pinMode(FUN_2_IN1, OUTPUT);
  pinMode(FUN_2_IN2, OUTPUT);

  //关闭所有风扇
  digitalWrite(FUN_1_IN1, LOW);
  digitalWrite(FUN_1_IN2, LOW);
  digitalWrite(FUN_2_IN1, LOW);
  digitalWrite(FUN_2_IN2, LOW);

  //修改PWM频率为10KHz
  analogWriteFreq(10000);

  sensors.begin();

  //初始化全局参数
  gFunControlMCB.autoControl = false;
  gFunControlMCB.autoReport = false;
  gFunControlMCB.firstReport = false;
  gFunControlMCB.fun1Speed = 0;
  gFunControlMCB.fun2Speed = 0;
  gFunControlMCB.temperature = 0;
  gFunControlMCB.autoControlTemp = 40.0;
  gFunControlMCB.tempAlarm = true;
  gFunControlMCB.tempLimit = 60.0;

  Serial.println("[System] Hardware Init OK.");

  if (digitalRead(KEY1) == 0)
  {
    Serial.println("[System] Reset Network Key Pressed, Strat SmartConfig:");
    smartConfig();
  }
  else if (!AutoConfig())
  {
    Serial.println("[System] Start SmartConfig......");
    smartConfig();
  }

  //MQTT Connect
  client.setServer(mqtt_server, 1883); //连接服务器
  client.setCallback(callback);        //设置回调函数

  //初始化MAC地址数组
  WiFi.macAddress(gFunControlMCB.MAC_adress);

  //初始化NTP服务
  timeClient.begin();
  update_time();

  //集中上报一次数据
  report_data();

  //设置数据上报定时器
  reportDataTimer.attach(30.0, report_data);
  reflashNTP.attach(90.0, update_time);
  temperatureMonitor.attach(10, tempC_adj);

  //开启看门口
  ESP.wdtEnable(5000);

  Serial.println("----------------------------------");
  Serial.println("Welcome to FunControl!");
  Serial.print("Author: Hbber    Version:");
  Serial.println(gFunControlMCB.Version);
  Serial.printf("SSID:%s", WiFi.SSID().c_str());
  Serial.printf("    PSW:%s\r\n", WiFi.psk().c_str());
  Serial.print("LocalIP:");
  Serial.print(WiFi.localIP());
  Serial.print("    GateIP:");
  Serial.println(WiFi.gatewayIP());
  Serial.print("MQTT Broke:");
  Serial.println(mqtt_server);
  Serial.println("----------------------------------");
}

void loop()
{
  ESP.wdtFeed();
  //检查MQTT是否掉线，自动重连
  if (!client.connected())
  {
    reconnect();
  }

  //维持MQTT
  client.loop();
}